package org.jetbrains.plugins.scala.util

import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.{Project, ProjectUtil}
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.{PsiComment, PsiFile, PsiManager}
import org.jetbrains.annotations.{NotNull, Nullable}
import org.jetbrains.plugins.scala.extensions.{PathExt, ToNullSafe}
import org.jetbrains.plugins.scala.lang.lexer.ScalaTokenTypes
import org.junit.Assert
import org.junit.Assert.{assertNotNull, fail}

import java.io.IOException
import java.net.URISyntaxException
import java.nio.charset.Charset
import java.nio.file.Path
import java.util
import scala.util.chaining.scalaUtilChainingOps

object TestUtils {

  private val LOG = Logger.getInstance("org.jetbrains.plugins.scala.util.TestUtils")

  val CARET_MARKER = "<caret>"
  val BEGIN_MARKER = "<begin>"
  val END_MARKER = "<end>"

  private var TEST_DATA_PATH: String = _

  def getTestDataDir: Path =
    Path.of(getTestDataPath).toCanonicalPath

  def getTestDataPath: String = {
    if (TEST_DATA_PATH == null) {
      try {
        val resource = TestUtils.getClass.getClassLoader.getResource("testdata")
        TEST_DATA_PATH =
          if (resource == null)
            find("scala/scala-impl", "testdata").toAbsolutePath.toString
          else
            Path.of(resource.toURI).systemIndependentPathString
      } catch {
        case e@(_: URISyntaxException | _: IOException) =>
          LOG.error(e)
          // just rethrowing here because that's a clearer way to make tests fail than some NPE somewhere else
          throw new RuntimeException(e)
      }
    }
    TEST_DATA_PATH
  }

  @throws[IOException]
  def findCommunityRoot: String = {
    // <community-root>/scala/scala-impl/testdata/
    val testDataPath = getTestDataPath
    Path.of(testDataPath, "..", "..", "..").normalize.toString + "/"
  }

  def findCommunityRootPath: Path =
    Path.of(findCommunityRoot).normalize()

  def findUltimateRootPath: Path = {
    val community = findCommunityRootPath
    community.getParent.normalize()
  }

  @throws[IOException]
  def findTestDataDir(pathname: String): String =
    findTestDataDir(Path.of(pathname), "testdata")

  @throws[IOException]
  private def find(pathname: String, child: String): Path = {
    val file = Path.of("community", pathname, child)
    if (file.exists) file
    else Path.of(findTestDataDir(pathname))
  }

  /** Go upwards to find testdata, because when running test from IDEA, the launching dir might be some subdirectory. */
  @throws[IOException]
  private def findTestDataDir(parent: Path, child: String): String = {
    val testData = (parent / child).toCanonicalPath
    if (testData.exists)
      testData.toString
    else {
      val newParent = parent.toCanonicalPath.getParent
      if (newParent == null) throw new RuntimeException("no testdata directory found")
      else findTestDataDir(newParent, child)
    }
  }

  def removeBeginMarker(text: String): String = {
    val index = text.indexOf(BEGIN_MARKER)
    text.substring(0, index) + text.substring(index + BEGIN_MARKER.length)
  }

  def removeEndMarker(text: String): String = {
    val index = text.indexOf(END_MARKER)
    text.substring(0, index) + text.substring(index + END_MARKER.length)
  }

  @throws[IOException]
  def readInput(filePath: String): util.List[String] = readInput(Path.of(filePath), null)

  @throws[IOException]
  def readInput(file: Path, @Nullable encoding: Charset, allowEmptyFiles: Boolean = false): util.List[String] = {
    var content = file.readAllBytesToString(encoding.nullSafe.getOrElse(Charset.defaultCharset))
    Assert.assertNotNull(content)

    if (allowEmptyFiles && content.isEmpty) return util.Collections.emptyList()

    val input = new util.ArrayList[String]
    var separatorIndex = 0
    content = StringUtil.replace(content, "\r", "") // for MACs

    // Adding input  before -----
    separatorIndex = content.indexOf("-----")
    while (separatorIndex >= 0) {
      input.add(content.substring(0, separatorIndex - 1))
      content = content.substring(separatorIndex)
      while (StringUtil.startsWithChar(content, '-')) {
        content = content.substring(1)
      }
      if (StringUtil.startsWithChar(content, '\n')) {
        content = content.substring(1)
      }
      separatorIndex = content.indexOf("-----")
    }
    // Result - after -----
    if (content.endsWith("\n")) {
      content = content.substring(0, content.length - 1)
    }
    input.add(content)
    Assert.assertTrue("No data found in source file", input.size > 0)
    input
  }

  /**
   * @param fileText text of the file without last comment
   * @param expectedResult content of the last comment in teh file
   */
  case class ExpectedResultFromLastComment(fileText: String, expectedResult: String)

  /**
   * A lot of file-based tests contain their test data in the last comment in the file.<br>
   * For example for some formatter tests the file could look like:
   * {{{
   *   class   ClassForFormat   {     }
   *   /*
   *   class ClassForFormat {}
   *   */
   * }}}
   *
   * @param file file which was created from test data which contains comment in the end of the file
   * @return 1. file content without last comment<br>
   *         2. last comment content as an expected result
   */
  def extractExpectedResultFromLastComment(file: PsiFile): ExpectedResultFromLastComment = {
    val fileText = file.getText

    val lastComment = file.findElementAt(fileText.length - 1) match {
      case comment: PsiComment => comment
      case element =>
        fail(s"Last element in the file is expected to be a comment but got: ${element.getClass} with text: ${element.getText}").asInstanceOf[Nothing]
    }

    val fileTextWithoutLastComment = file.getText.substring(0, lastComment.getTextOffset).trim

    val commentText = lastComment.getText
    val commentInnerContent = lastComment.getNode.getElementType match {
      case ScalaTokenTypes.tLINE_COMMENT => commentText.substring(2)
      case ScalaTokenTypes.tBLOCK_COMMENT=> commentText.substring(2, commentText.length - 2)
      case ScalaTokenTypes.tDOC_COMMENT => commentText.substring(3, commentText.length - 2)
      case _ =>
        fail("Test result must be in last comment statement.").asInstanceOf[Nothing]
    }
    ExpectedResultFromLastComment(fileTextWithoutLastComment, commentInnerContent.trim)
  }

  @NotNull
  @throws[AssertionError]
  def guessProjectDir(@NotNull project: Project): VirtualFile = ProjectUtil.guessProjectDir(project).tap { file =>
    assertNotNull(s"Cannot guess directory of project ${project.getName}", file)
  }

  @throws[AssertionError]
  def getPathRelativeToProject(file: VirtualFile, @NotNull project: Project): String = {
    val projectRoot = guessProjectDir(project)
    val pathParent = projectRoot.getPath
    val pathChild = file.getPath
    pathChild.stripPrefix(pathParent).stripPrefix("/")
  }

  @NotNull
  @throws[AssertionError]
  def findFileInProject(@NotNull project: Project, relativePath: String): PsiFile = {
    val projectDir = guessProjectDir(project)

    val vFile = projectDir.findFileByRelativePath(relativePath)
    assertNotNull(s"Can't find virtual file ${projectDir.getCanonicalPath}/$relativePath", vFile)

    //noinspection DfaNullableToNotNullParam
    PsiManager.getInstance(project).findFile(vFile).tap { psiFile =>
      assertNotNull(s"Can't psi file for $vFile", psiFile)
    }
  }
}
